// Copyright (c) 2023 CHANGLEI. All rights reserved.

import 'package:flutter/gestures.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_grasp/src/widget/animated_offset.dart';
import 'package:flutter_grasp/src/widget/dragging_auto_scroller.dart';
import 'package:flutter_grasp/src/widget/geometry.dart';

const double _autoScrollVelocityScalar = 30;

/// Created by changlei on 2020/8/26.
///
/// 系统[Draggable]加动画
class AnimatedDraggable<T extends Object> extends StatefulWidget {
  /// 加了动画的[Draggable]
  const AnimatedDraggable({
    super.key,
    required this.child,
    this.feedback,
    this.data,
    this.axis,
    this.childWhenDragging,
    this.feedbackOffset = Offset.zero,
    this.dragAnchorStrategy = childDragAnchorStrategy,
    this.affinity,
    this.maxSimultaneousDrags,
    this.onDragStart,
    this.onDragStarted,
    this.onDragUpdate,
    this.onDraggableCanceled,
    this.onDragEnd,
    this.onDragEnded,
    this.onDragCompleted,
    this.ignoringFeedbackSemantics = true,
    this.ignoringFeedbackPointer = true,
    this.tickerOffset = true,
    this.rootOverlay = false,
    this.hitTestBehavior = HitTestBehavior.deferToChild,
    this.allowedButtonsFilter,
    this.autoScrollBoundary,
    required this.duration,
    this.curve = Curves.linear,
    this.alignment = Alignment.center,
  }) : assert(maxSimultaneousDrags == null || maxSimultaneousDrags >= 0);

  /// The data that will be dropped by this draggable.
  final T? data;

  /// The [Axis] to restrict this draggable's movement, if specified.
  ///
  /// When axis is set to [Axis.horizontal], this widget can only be dragged
  /// horizontally. Behavior is similar for [Axis.vertical].
  ///
  /// Defaults to allowing drag on both [Axis.horizontal] and [Axis.vertical].
  ///
  /// When null, allows drag on both [Axis.horizontal] and [Axis.vertical].
  ///
  /// For the direction of gestures this widget competes with to start a drag
  /// event, see [Draggable.affinity].
  final Axis? axis;

  /// The widget below this widget in the tree.
  ///
  /// This widget displays [child] when zero drags are under way. If
  /// [childWhenDragging] is non-null, this widget instead displays
  /// [childWhenDragging] when one or more drags are underway. Otherwise, this
  /// widget always displays [child].
  ///
  /// The [feedback] widget is shown under the pointer when a drag is under way.
  ///
  /// To limit the number of simultaneous drags on multitouch devices, see
  /// [maxSimultaneousDrags].
  ///
  /// {@macro flutter.widgets.child}
  final Widget child;

  /// The widget to display instead of [child] when one or more drags are under way.
  ///
  /// If this is null, then this widget will always display [child] (and so the
  /// drag source representation will not change while a drag is under
  /// way).
  ///
  /// The [feedback] widget is shown under the pointer when a drag is under way.
  ///
  /// To limit the number of simultaneous drags on multitouch devices, see
  /// [maxSimultaneousDrags].
  final Widget? childWhenDragging;

  /// The widget to show under the pointer when a drag is under way.
  ///
  /// See [child] and [childWhenDragging] for information about what is shown
  /// at the location of the [Draggable] itself when a drag is under way.
  final Widget? feedback;

  /// The feedbackOffset can be used to set the hit test target point for the
  /// purposes of finding a drag target. It is especially useful if the feedback
  /// is transformed compared to the child.
  final Offset feedbackOffset;

  /// A strategy that is used by this draggable to get the anchor offset when it
  /// is dragged.
  ///
  /// The anchor offset refers to the distance between the users' fingers and
  /// the [feedback] widget when this draggable is dragged.
  ///
  /// This property's value is a function that implements [DragAnchorStrategy].
  /// There are two built-in functions that can be used:
  ///
  ///  * [childDragAnchorStrategy], which displays the feedback anchored at the
  ///    position of the original child.
  ///
  ///  * [pointerDragAnchorStrategy], which displays the feedback anchored at the
  ///    position of the touch that started the drag.
  final DragAnchorStrategy dragAnchorStrategy;

  /// Whether the semantics of the [feedback] widget is ignored when building
  /// the semantics tree.
  ///
  /// This value should be set to false when the [feedback] widget is intended
  /// to be the same object as the [child].  Placing a [GlobalKey] on this
  /// widget will ensure semantic focus is kept on the element as it moves in
  /// and out of the feedback position.
  ///
  /// Defaults to true.
  final bool ignoringFeedbackSemantics;

  /// Whether the [feedback] widget is ignored during hit testing.
  ///
  /// Regardless of whether this widget is ignored during hit testing, it will
  /// still consume space during layout and be visible during painting.
  ///
  /// Defaults to true.
  final bool ignoringFeedbackPointer;

  /// Controls how this widget competes with other gestures to initiate a drag.
  ///
  /// If affinity is null, this widget initiates a drag as soon as it recognizes
  /// a tap down gesture, regardless of any directionality. If affinity is
  /// horizontal (or vertical), then this widget will compete with other
  /// horizontal (or vertical, respectively) gestures.
  ///
  /// For example, if this widget is placed in a vertically scrolling region and
  /// has horizontal affinity, pointer motion in the vertical direction will
  /// result in a scroll and pointer motion in the horizontal direction will
  /// result in a drag. Conversely, if the widget has a null or vertical
  /// affinity, pointer motion in any direction will result in a drag rather
  /// than in a scroll because the draggable widget, being the more specific
  /// widget, will out-compete the [Scrollable] for vertical gestures.
  ///
  /// For the directions this widget can be dragged in after the drag event
  /// starts, see [Draggable.axis].
  final Axis? affinity;

  /// How many simultaneous drags to support.
  ///
  /// When null, no limit is applied. Set this to 1 if you want to only allow
  /// the drag source to have one item dragged at a time. Set this to 0 if you
  /// want to prevent the draggable from actually being dragged.
  ///
  /// If you set this property to 1, consider supplying an "empty" widget for
  /// [childWhenDragging] to create the illusion of actually moving [child].
  final int? maxSimultaneousDrags;

  /// 拖动即将开始，即按下的时候
  final GestureDragStartCallback? onDragStart;

  /// Called when the draggable starts being dragged.
  final VoidCallback? onDragStarted;

  /// Called when the draggable is dragged.
  ///
  /// This function will only be called while this widget is still mounted to
  /// the tree (i.e. [State.mounted] is true), and if this widget has actually moved.
  final DragUpdateCallback? onDragUpdate;

  /// Called when the draggable is dropped without being accepted by a [DragTarget].
  ///
  /// This function might be called after this widget has been removed from the
  /// tree. For example, if a drag was in progress when this widget was removed
  /// from the tree and the drag ended up being canceled, this callback will
  /// still be called. For this reason, implementations of this callback might
  /// need to check [State.mounted] to check whether the state receiving the
  /// callback is still in the tree.
  final DraggableCanceledCallback? onDraggableCanceled;

  /// Called when the draggable is dropped and accepted by a [DragTarget].
  ///
  /// This function might be called after this widget has been removed from the
  /// tree. For example, if a drag was in progress when this widget was removed
  /// from the tree and the drag ended up completing, this callback will
  /// still be called. For this reason, implementations of this callback might
  /// need to check [State.mounted] to check whether the state receiving the
  /// callback is still in the tree.
  final VoidCallback? onDragCompleted;

  /// Called when the draggable is dropped.
  ///
  /// The velocity and offset at which the pointer was moving when it was
  /// dropped is available in the [DraggableDetails]. Also included in the
  /// `details` is whether the draggable's [DragTarget] accepted it.
  ///
  /// This function will only be called while this widget is still mounted to
  /// the tree (i.e. [State.mounted] is true).
  final DragEndCallback? onDragEnd;

  /// 拖动结束切动画一并结束了
  final DragEndCallback? onDragEnded;

  /// 自动滚动边界
  final AutoScrollBoundary? autoScrollBoundary;

  /// The length of time this animation should last.
  final Duration duration;

  /// The curve to use in the forward direction.
  final Curve curve;

  /// The alignment of the child within the parent when the parent is not yet
  /// the same size as the child.
  final AlignmentGeometry alignment;

  /// 是否响应offset动画
  final bool tickerOffset;

  /// Whether the feedback widget will be put on the root [Overlay].
  ///
  /// When false, the feedback widget will be put on the closest [Overlay]. When
  /// true, the [feedback] widget will be put on the farthest (aka root)
  /// [Overlay].
  ///
  /// Defaults to false.
  final bool rootOverlay;

  /// How to behave during hit test.
  ///
  /// Defaults to [HitTestBehavior.deferToChild].
  final HitTestBehavior hitTestBehavior;

  /// {@macro flutter.gestures.multidrag._allowedButtonsFilter}
  final AllowedButtonsFilter? allowedButtonsFilter;

  /// Whether haptic feedback should be triggered on drag start.
  bool get _hapticFeedbackOnStart => true;

  bool get _isLongPressDrag => false;

  /// The duration that a user has to press down before a long press is registered.
  ///
  /// Defaults to [kLongPressTimeout].
  Duration get _delay => Duration.zero;

  /// 获取[feedback]动画
  static Animation<double>? feedbackAnimation(BuildContext context) {
    return _FeedbackAnimationScope.of(context);
  }

  /// 获取[childWhenDragging]动画
  static Animation<double>? childWhenDraggingAnimation(BuildContext context) {
    return _ChildWhenDraggingAnimationScope.of(context);
  }

  @override
  State<AnimatedDraggable<T>> createState() => _AnimatedDraggableState<T>();
}

class _AnimatedDraggableState<T extends Object> extends State<AnimatedDraggable<T>> with TickerProviderStateMixin {
  late AnimationController _controller;
  late CurvedAnimation _curvedAnimation;
  late AnimationController _placeholderController;

  _DragEndAvatar? _dragEndAvatar;
  SizeTween? _placeholderTween;
  Size? _originSize;
  Offset? _dragPosition;
  Offset? _dragOffset;
  ScrollableState? _scrollable;
  DraggingAutoScroller? _autoScroller;
  bool _dragStarted = false;

  late final _placeholderAnimation = CurvedAnimation(
    parent: _placeholderController,
    curve: widget.curve,
  );

  final _dragUpdater = ValueNotifier<DragUpdateDetails?>(null);

  @override
  void initState() {
    _controller = AnimationController(
      vsync: this,
      duration: widget.duration,
    );
    _curvedAnimation = CurvedAnimation(
      parent: _controller,
      curve: widget.curve,
    );
    _placeholderController = AnimationController(
      vsync: this,
      duration: widget.duration,
    );
    super.initState();
  }

  Axis? get _scrollDirection {
    final scrollable = _scrollable;
    return scrollable == null ? null : axisDirectionToAxis(scrollable.axisDirection);
  }

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    final scrollable = Scrollable.maybeOf(context);
    _scrollable = scrollable;
    if (_autoScroller?.scrollable != scrollable) {
      _autoScroller?.stopAutoScroll();
      _autoScroller = null;
      if (scrollable != null) {
        _autoScroller = DraggingAutoScroller(
          scrollable,
          velocityScalar: _autoScrollVelocityScalar,
          onScrollViewScrolled: _startAutoScrollIfNecessary,
        );
      }
    }
  }

  void _startAutoScrollIfNecessary() {
    final autoScroller = _autoScroller;
    if (mounted && _dragStarted && autoScroller != null) {
      final target = (_dragPosition! - _dragOffset!) & _lastSize!;
      final scrollRenderBox = autoScroller.scrollable.context.findRenderObject()! as RenderBox;
      final globalRect = MatrixUtils.transformRect(
        scrollRenderBox.getTransformTo(null),
        Rect.fromLTWH(0, 0, scrollRenderBox.size.width, scrollRenderBox.size.height),
      );
      if (globalRect.size.width >= target.size.width && globalRect.size.height >= target.size.height) {
        autoScroller.startAutoScrollIfNecessary(target, widget.autoScrollBoundary);
      }
    }
  }

  void _onSizeChanged() {
    SchedulerBinding.instance.addPostFrameCallback((_) {
      if (!mounted || !_dragStarted) {
        return;
      }
      final currentSize = context.size ?? Size.zero;
      final lastSize = _lastSize ?? Size.zero;
      if (currentSize == lastSize) {
        return;
      }

      _placeholderTween = SizeTween(
        begin: lastSize,
        end: currentSize,
      );
      _controller.forward(
        from: _controller.lowerBound,
      );
    });
  }

  @override
  void dispose() {
    _controller.dispose();
    _placeholderController.dispose();
    if (_dragEndAvatar != null) {
      _disposeVariableIfInactive();
    }
    _dragUpdater.dispose();
    super.dispose();
  }

  void _disposeVariableIfInactive() {
    _dragEndAvatar?.dispose();
    _dragEndAvatar = null;
    _placeholderTween = null;
    _originSize = null;
    _dragStarted = false;
    _autoScroller?.stopAutoScroll();
    _autoScroller = null;
    _dragPosition = null;
    _dragOffset = null;
    _scrollable = null;
  }

  Size? get _lastSize => _placeholderTween?.evaluate(_curvedAnimation);

  void _onDragStarted() {
    _placeholderController.forward();
    final currentSize = context.size ?? Size.zero;
    _originSize = currentSize;

    _placeholderTween = SizeTween(
      begin: currentSize,
      end: currentSize,
    );
    _controller.forward(
      from: _controller.lowerBound,
    );

    _dragStarted = true;
    _startAutoScrollIfNecessary();
    widget.onDragStarted?.call();
  }

  void _onDragUpdate(DragUpdateDetails details) {
    _dragUpdater.value = details;
    final scrollDirection = _scrollDirection;
    if (scrollDirection != null) {
      final delta = _restrictAxis(details.delta, scrollDirection);
      final dragPosition = _dragPosition;
      if (dragPosition != null) {
        _dragPosition = delta + dragPosition;
      }
    }
    _startAutoScrollIfNecessary();
    widget.onDragUpdate?.call(details);
  }

  void _onDragEnd(DraggableDetails details) {
    _dragStarted = false;
    _autoScroller?.stopAutoScroll();
    _placeholderController.reverse();
    final lastSize = _lastSize ?? Size.zero;
    final tickerFuture = _controller.reverse(
      from: _controller.upperBound,
    );
    tickerFuture.whenCompleteOrCancel(() {
      if (!mounted) {
        return;
      }
      widget.onDragEnded?.call(details);
      setState(() {
        _placeholderTween = null;
        _originSize = null;
        _dragEndAvatar?.dispose();
        _dragEndAvatar = null;
        _dragPosition = null;
        _dragUpdater.value = null;
      });
    });
    final beginRect = -globalPosition(context) & context.size!;
    final endRect = details.offset & lastSize;
    final animation = _curvedAnimation.drive(
      RectTween(
        begin: beginRect,
        end: endRect,
      ),
    );
    _dragEndAvatar?.dispose();
    _dragEndAvatar = _DragEndAvatar(
      overlay: Overlay.of(
        context,
        debugRequiredFor: widget,
        rootOverlay: widget.rootOverlay,
      ),
      animation: animation,
      child: _feedback,
    );

    widget.onDragEnd?.call(details);
  }

  void _onDraggableCanceled(Velocity velocity, Offset offset) {
    if (!mounted) {
      _disposeVariableIfInactive();
    }
    widget.onDraggableCanceled?.call(velocity, offset);
  }

  void _onDragCompleted() {
    if (!mounted) {
      _disposeVariableIfInactive();
    }
    widget.onDragCompleted?.call();
  }

  Widget _buildChild(BuildContext context) {
    if (_dragEndAvatar == null) {
      return widget.child;
    } else {
      return _buildChildWhenDragging(context);
    }
  }

  Widget get _feedback {
    return InheritedTheme.captureAll(
      context,
      _FeedbackAnimationScope(
        animation: _placeholderAnimation,
        child: widget.feedback ?? widget.child,
      ),
    );
  }

  Widget _buildFeedback(BuildContext context) {
    final originSize = _originSize ?? Size.zero;
    return AnimatedBuilder(
      animation: _curvedAnimation,
      builder: (context, child) {
        return SizedBox.fromSize(
          size: _placeholderTween?.evaluate(_curvedAnimation) ?? originSize,
          child: child,
        );
      },
      child: ValueListenableBuilder(
        valueListenable: _dragUpdater,
        builder: (context, value, child) {
          return _feedback;
        },
      ),
    );
  }

  Widget _buildChildWhenDragging(BuildContext context) {
    final childWhenDragging = widget.childWhenDragging;
    if (childWhenDragging == null) {
      return Visibility.maintain(
        visible: false,
        child: widget.child,
      );
    } else {
      return _ChildWhenDraggingAnimationScope(
        animation: _placeholderAnimation,
        child: childWhenDragging,
      );
    }
  }

  Widget _buildDraggable() {
    return Draggable<T>(
      feedback: Builder(
        builder: _buildFeedback,
      ),
      childWhenDragging: Builder(
        builder: _buildChildWhenDragging,
      ),
      data: widget.data,
      maxSimultaneousDrags: widget.maxSimultaneousDrags,
      onDragStarted: _onDragStarted,
      onDragUpdate: _onDragUpdate,
      onDragEnd: _onDragEnd,
      onDraggableCanceled: _onDraggableCanceled,
      onDragCompleted: _onDragCompleted,
      dragAnchorStrategy: widget.dragAnchorStrategy,
      feedbackOffset: widget.feedbackOffset,
      axis: widget.axis,
      ignoringFeedbackSemantics: widget.ignoringFeedbackSemantics,
      ignoringFeedbackPointer: widget.ignoringFeedbackPointer,
      affinity: widget.affinity,
      hitTestBehavior: widget.hitTestBehavior,
      allowedButtonsFilter: widget.allowedButtonsFilter,
      rootOverlay: widget.rootOverlay,
      child: Builder(
        builder: _buildChild,
      ),
    );
  }

  Widget _buildLongPressDraggable() {
    return LongPressDraggable<T>(
      feedback: Builder(
        builder: _buildFeedback,
      ),
      childWhenDragging: Builder(
        builder: _buildChildWhenDragging,
      ),
      data: widget.data,
      maxSimultaneousDrags: widget.maxSimultaneousDrags,
      hapticFeedbackOnStart: widget._hapticFeedbackOnStart,
      onDragStarted: _onDragStarted,
      onDragUpdate: _onDragUpdate,
      onDragEnd: _onDragEnd,
      onDraggableCanceled: _onDraggableCanceled,
      onDragCompleted: _onDragCompleted,
      dragAnchorStrategy: widget.dragAnchorStrategy,
      feedbackOffset: widget.feedbackOffset,
      axis: widget.axis,
      ignoringFeedbackSemantics: widget.ignoringFeedbackSemantics,
      delay: widget._delay,
      allowedButtonsFilter: widget.allowedButtonsFilter,
      ignoringFeedbackPointer: widget.ignoringFeedbackPointer,
      child: Builder(
        builder: _buildChild,
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return NotificationListener<SizeChangedLayoutNotification>(
      onNotification: (notification) {
        _onSizeChanged();
        return false;
      },
      child: SizeChangedLayoutNotifier(
        child: Listener(
          onPointerDown: (event) {
            final position = event.position;
            final renderObject = context.findRenderObject() as RenderBox;
            final dragOffset = renderObject.globalToLocal(position);
            final dragPosition = position;
            _dragOffset = dragOffset;
            _dragPosition = dragPosition;
            widget.onDragStart?.call(
              DragStartDetails(
                kind: event.kind,
                sourceTimeStamp: event.timeStamp,
                globalPosition: dragOffset,
                localPosition: dragPosition,
              ),
            );
          },
          child: AnimatedOffset(
            alignment: widget.alignment,
            duration: widget.tickerOffset ? widget.duration : Duration.zero,
            curve: widget.curve,
            child: widget._isLongPressDrag ? _buildLongPressDraggable() : _buildDraggable(),
          ),
        ),
      ),
    );
  }
}

/// Makes its child draggable starting from long press.
class AnimatedLongPressDraggable<T extends Object> extends AnimatedDraggable<T> {
  /// Creates a widget that can be dragged starting from long press.
  ///
  /// The [child] and [feedback] arguments must not be null. If
  /// [maxSimultaneousDrags] is non-null, it must be non-negative.
  const AnimatedLongPressDraggable({
    super.key,
    required super.child,
    super.feedback,
    super.data,
    super.axis,
    super.childWhenDragging,
    super.feedbackOffset,
    super.dragAnchorStrategy,
    super.maxSimultaneousDrags,
    super.onDragStart,
    super.onDragStarted,
    super.onDragUpdate,
    super.onDraggableCanceled,
    super.onDragEnd,
    super.onDragEnded,
    super.onDragCompleted,
    this.hapticFeedbackOnStart = true,
    super.ignoringFeedbackSemantics,
    super.ignoringFeedbackPointer,
    this.delay = kLongPressTimeout,
    super.allowedButtonsFilter,
    super.autoScrollBoundary,
    required super.duration,
    super.curve,
    super.alignment,
    super.tickerOffset,
  });

  /// Whether haptic feedback should be triggered on drag start.
  final bool hapticFeedbackOnStart;

  /// The duration that a user has to press down before a long press is registered.
  ///
  /// Defaults to [kLongPressTimeout].
  final Duration delay;

  @override
  bool get _hapticFeedbackOnStart => hapticFeedbackOnStart;

  @override
  bool get _isLongPressDrag => true;

  @override
  Duration get _delay => delay;
}

final class _DragEndAvatar {
  _DragEndAvatar({
    required this.overlay,
    required this.animation,
    required this.child,
  }) {
    _entry = OverlayEntry(builder: _build);
    overlay.insert(_entry!);
    animation.addStatusListener(_onAnimationStatusChanged);
  }

  final OverlayState overlay;
  final Animation<Rect?> animation;
  final Widget child;

  OverlayEntry? _entry;

  Widget _build(BuildContext context) {
    final box = overlay.context.findRenderObject()! as RenderBox;
    final overlayTopLeft = box.localToGlobal(Offset.zero);
    return AnimatedBuilder(
      animation: animation,
      builder: (context, child) {
        return Positioned.fromRect(
          rect: animation.value?.shift(-overlayTopLeft) ?? Rect.zero,
          child: child!,
        );
      },
      child: child,
    );
  }

  void _onAnimationStatusChanged(AnimationStatus status) {
    animation.removeStatusListener(_onAnimationStatusChanged);
    switch (status) {
      case AnimationStatus.dismissed:
      case AnimationStatus.completed:
        _entry?.remove();
        _entry = null;
      case AnimationStatus.forward:
        break;
      case AnimationStatus.reverse:
        break;
    }
  }

  void dispose() {
    _entry?.remove();
    _entry = null;
  }
}

/// 下穿[Animation<double>]
class _FeedbackAnimationScope extends InheritedWidget {
  /// 下穿[Animation<double>]
  const _FeedbackAnimationScope({
    required super.child,
    required this.animation,
  });

  /// 下穿[Animation<double>]
  final Animation<double> animation;

  /// 获取
  static Animation<double>? of(BuildContext context) {
    return context.dependOnInheritedWidgetOfExactType<_FeedbackAnimationScope>()?.animation;
  }

  @override
  bool updateShouldNotify(covariant _FeedbackAnimationScope oldWidget) {
    return animation != oldWidget.animation;
  }
}

/// 下穿[Animation<double>]
class _ChildWhenDraggingAnimationScope extends InheritedWidget {
  /// 下穿[Animation<double>]
  const _ChildWhenDraggingAnimationScope({
    required super.child,
    required this.animation,
  });

  /// 下穿[Animation<double>]
  final Animation<double> animation;

  /// 获取
  static Animation<double>? of(BuildContext context) {
    return context.dependOnInheritedWidgetOfExactType<_ChildWhenDraggingAnimationScope>()?.animation;
  }

  @override
  bool updateShouldNotify(covariant _ChildWhenDraggingAnimationScope oldWidget) {
    return animation != oldWidget.animation;
  }
}

Offset _restrictAxis(Offset offset, Axis scrollDirection) {
  switch (scrollDirection) {
    case Axis.horizontal:
      return Offset(offset.dx, 0);
    case Axis.vertical:
      return Offset(0, offset.dy);
  }
}
